1 module hip.game2d.text;
2 
3 import hip.api.data.font;
4 import hip.api.graphics.text;
5 import hip.util.data_structures;
6 
7 
8 /**
9 *   Formatting the text:
10 * Text should be formatted using the $() syntax.
11 * Currently, no formatting is support, but that syntax is reserved and in the future, it
12 * will be used as for example: $(RGB, 1.0, 1.0, 1.0) or even $(WHITE), so, basic parsing
13 * is being done for accounting how many text does really need to be rendered.
14 */
15 class HipText
16 {
17     HipTextAlign alignh = HipTextAlign.LEFT;
18     HipTextAlign alignv = HipTextAlign.TOP;
19 
20     HipFont font;
21     int x, y;
22     bool wordWrap;
23 
24     DirtyFlagsCmp!(
25         shouldUpdateText, x, y, 
26         wordWrap, font,
27         alignh, alignv
28     ) checkDirty;
29 
30 
31     float depth = 0;
32     ///Update dynamically based on the font, the text scale and the text content
33     int width, height;
34 
35     int boundsWidth = -1, boundsHeight = -1;
36 
37     //Line widths, containing width for each line for correctly aplying text align
38     uint[] linesWidths;
39 
40     protected string _text;
41     protected dstring _dtext;
42     protected dstring processedText;
43     protected HipColor _color = HipColor.black;
44 
45     //Debugging?
46 
47     protected bool shouldRenderSpace = false;
48     protected bool shouldRenderLineBreak = false;
49 
50     protected HipTextStopConfig[] textConfig;
51     protected HipTextRendererVertexAPI[] vertices;
52 
53     //Caching
54     protected size_t _drawableTextCount = 0;
55     protected size_t maxDrawableTextCount = 0;
56     public bool shouldUpdateText = true;
57 
58     this(int boundsWidth = -1, int boundsHeight = -1, bool bWordWrap = false)
59     {
60         import hip.api;
61         checkDirty.start(this);
62         this.font = cast()HipDefaultAssets.getDefaultFont();
63         linesWidths.length = 1;
64         wordWrap = bWordWrap;
65         this.boundsWidth = boundsWidth;
66         this.boundsHeight = boundsHeight;
67     }
68     this(string text, int x, int y, HipFont fnt = null, int boundsWidth = -1, int boundsHeight = -1, bool bWordWrap = false)
69     {
70         this(boundsWidth, boundsHeight, bWordWrap);
71         this.setPosition(x,y);
72         this.text = text;
73         if(fnt) font = fnt;
74     }
75     string text() const {return _text;}
76     size_t drawableTextCount() const {return _drawableTextCount;}
77 
78     
79     string text(string newText)
80     {
81         if(newText != _text)
82         {
83             import hip.util.string;
84             dstring dtext = newText.toUTF32;
85             _drawableTextCount = countVertices(dtext);
86             shouldUpdateText = true;
87             if(_drawableTextCount > maxDrawableTextCount)
88             {
89                 //As it is a quad, it needs to have vertices * 4
90                 vertices.length = _drawableTextCount * 4;
91                 maxDrawableTextCount = _drawableTextCount;
92             }
93             _text = newText;
94             _dtext = dtext;
95         }
96         return _text;
97     }
98 
99     void setPosition(int x, int y)
100     {
101         this.x = x;
102         this.y = y;
103     }
104 
105     HipColor color() => _color;
106     HipColor color(HipColor c) => _color = c;
107 
108     void[] getVertices()
109     {
110         checkDirty();
111         if(shouldUpdateText)
112         {
113             updateText(font);
114             checkDirty.start(this);
115         }
116         
117         return cast(void[])vertices[0..drawableTextCount * 4];
118     }
119     
120     protected void updateAlign(int lineNumber, out int displayX, out int displayY, int boundsWidth, int boundsHeight)
121     {
122         import hip.api.graphics.text;
123         getPositionFromAlignment(x, y, linesWidths[lineNumber], height, alignh, alignv, displayX, displayY, boundsWidth, boundsHeight);
124     }
125     
126 
127     public void getSize(out int width, out int height)
128     {
129         if(processedText == null)
130             HipTextStopConfig.parseText(_dtext, processedText, textConfig);
131         font.calculateTextBounds(processedText, linesWidths, width, height, boundsWidth);
132         this.width = width;
133         this.height = height;
134     }
135     public void setAlign(HipTextAlign alignh, HipTextAlign alignv)
136     {
137         this.alignh = alignh;
138         this.alignv = alignv;
139     }
140 
141     package void updateText(IHipFont font)
142     {
143         HipTextStopConfig.parseText(_dtext, processedText, textConfig);
144         int vI = 0; //vertex buffer index
145 
146         bool isFirstLine = true;
147         int yoffset = 0;
148         foreach(HipLineInfo lineInfo; font.wordWrapRange(processedText, wordWrap ? boundsWidth : -1))
149         {
150             if(!isFirstLine)
151             {
152                 yoffset+= font.lineBreakHeight;
153             }
154             isFirstLine = false;
155             int xoffset = 0;
156             int displayX = void, displayY = void;
157             getPositionFromAlignment(x, y, lineInfo.width, height, alignh, alignv, displayX, displayY, boundsWidth, boundsHeight);
158             for(int i = 0; i < lineInfo.line.length; i++)
159             {
160                 int kerning = lineInfo.kerningCache[i];
161                 const(HipFontChar)* ch = lineInfo.fontCharCache[i];
162 
163                 switch(lineInfo.line[i])
164                 {
165                     case ' ':
166                         if(!shouldRenderSpace)
167                         {
168                             xoffset+= font.spaceWidth;
169                             break;
170                         }
171                         goto default;
172                     default:
173                         if(ch is null) continue;
174                         ch.putCharacterQuad(
175                             cast(float)(xoffset+displayX+ch.xoffset+kerning),
176                             cast(float)(yoffset+displayY+ch.yoffset), depth,
177                             vertices[vI..vI+4]
178                         );
179                         vI+= 4;
180                         xoffset+= ch.xadvance;
181                 }
182             }
183         }
184         shouldUpdateText = false;
185     }
186 
187     void draw()
188     {
189         import hip.api.graphics.g2d.g2d_binding;
190         setTextColor(color);
191         drawTextVertices(getVertices, font);
192     }
193 }
194 
195 
196 /**
197 *   The text stop config defines how this text will behave a
198 */
199 package struct HipTextStopConfig
200 {
201     import hip.api.graphics.color;
202     int startIndex;
203     HipColorf color;
204 
205     //This is just a plan, not supported right now
206     public static enum Tokens
207     {
208         alignh = "alignh",
209         alignv = "alignv",
210         rgb = "rgb",
211         color = "color",
212         bold = "bold",
213         italic = "italic",
214         red = "red",
215         green = "green",
216         blue = "blue",
217     }
218 
219     private static HipTextStopConfig parseToken(in dstring text, size_t indexToParse, out size_t continueIndex)
220     {
221         import hip.util.conv;
222         import hip.util.string;
223         import hip.util.algorithm;
224         int endIndex = text[indexToParse..$].indexOf(")"); //Won't support parenthesis between them.
225         assert(endIndex != -1, "Missing ending parenthesis on string at HipTextStopConfig formatting ");
226         continueIndex = endIndex+indexToParse;
227 
228 
229         auto range = splitRange(text[indexToParse..endIndex], ",");
230         dstring token = range.front;
231         range.popFront();
232 
233         switch(token)
234         {
235             case "rgb":
236             {
237                 HipColorf c = HipColorf(0, 0, 0, 1.0);
238                 range.map!((x) => x.trim.to!float).put(&c.r, &c.g, &c.b);
239                 return HipTextStopConfig(cast(int)indexToParse, c);
240             }
241             default: break;
242         }
243         return HipTextStopConfig(cast(int)indexToParse, cast()HipColorf.white);
244     }
245 
246 
247     static void parseText(in dstring text, out dstring parsedText, ref HipTextStopConfig[] config)
248     {
249         parsedText = text;
250         // size_t indexConfig = 0;
251         // size_t lastParseIndex = 0;
252         // dstring parsingText;
253         // for(size_t i = 0; i < text.length; i++)
254         // {
255         //     if(i+1 < text.length && text[i] == '$' && text[i+1] == '(') //Found something to parse
256         //     {
257         //         parsingText~= text[lastParseIndex..i-1];
258         //         HipTextStopConfig cfg = parseToken(text, i+1, i); //Update i
259         //         lastParseIndex = i;
260         //         if(indexConfig >= config.length)
261         //             config.length++;
262         //         config[indexConfig++] = cfg;
263         //     }
264         // }
265         // //!FIXME: This allocated on each frame. It should both be used a @nogc operation (String) or it should 
266         // //!find a way to create a range to be used instead of a string.
267         // if(lastParseIndex == 0)
268         // {
269         //     parsedText = text;
270         //     return;
271         // }
272         // parsedText = parsingText ~ text[lastParseIndex..$];
273     }
274 
275 }
276 
277 
278 
279 private size_t countVertices(dstring str)
280 {
281     size_t i = 0;
282     foreach(ch; str)
283     {
284         if(ch != ' ' && ch != '\n')
285             i++;
286     }
287     return i;
288 }